﻿// Copyright (c) Microsoft Corporation.  All rights reserved. 
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using WiE.net.windows.database;
using WiE.Utils;
using System.Web.Services.Protocols;

namespace WiE.Model
{
    /// <summary>
    /// Remote implementation of the Data Model that uses the Sitka SSDS cloud storage as medium for storing and retriving data objects.
    /// </summary>
    class WiEModelRemoteSSDSForMobile : IWiEModel
    {
        // Sitka Credentials
        // todo you need to insert your own SDS LOGIN and PASSWORD here and remove the #warning
#warning REMEMBER to update WiERemodeSSDSForMobile.cs to have your SQL Data Services credentials.
        private const string LOGIN_NAME = "<YOUR SDS USER NAME HERE>";
        private const string LOGIN_PASSWORD = "<YOUR SDS PASSWORD HERE>";
        private const string AUTHORITY_NAME = "wie";

        // Container
        private const string CONTAINER_NAME = "wie";

        // Entity Types
        private const string LOCATION_KIND = "location";
        private const string MEMBER_KIND = "member";
        private const string DEVICE_KIND = "device";

        // SOAP Web Service wrapper / proxy to Sitka SSDS Data service
        SitkaSoapService m_sitkaSoapService;

        /// <summary>
        /// Helper method that generates an SSDS Entity based on a WhereIsEd DataObject derived class by converting the
        /// DataObject into a property bag and then populating the entity's property array based on the DataObject's property bag.
        /// </summary>
        /// <param name="p_dataObject"></param>
        /// <returns></returns>
        private Entity DataObjectToEntity(WiEDataObject p_dataObject)
        {
            // Retrieve the data object in a property bag form
            Dictionary<string, object> dataObjectProperties = p_dataObject.ToPropertyDictionary();

            // Create a new entity object and initialize it
            Entity entity = new Entity();

            // Now create properties on the Entity to contain the values from the Data Object properties
            entity.Properties = new ArrayOfKeyValueOfstringanyTypeKeyValueOfstringanyType[dataObjectProperties.Count];
            int i = 0;
            foreach (KeyValuePair<string, object> kvp in dataObjectProperties)
            {
                entity.Properties[i] = new ArrayOfKeyValueOfstringanyTypeKeyValueOfstringanyType();
                entity.Properties[i].Key = kvp.Key;
                entity.Properties[i].Value = kvp.Value;
                i++;
            }

            // Return the entity with populate properties... still needs to have its Type and ID set.
            return entity;
        }

        /// <summary>
        /// Helper method to turn an Entity object's Array of properties into a Dictionary
        /// </summary>
        /// <remarks>
        /// This will become unecessary when I can use save web service mechanism on both the client and the
        /// other layers (i.e. WCF support).  Right now I have inconsistency in that the mobile client returns
        /// arrays of entities and the other clients have lists of entities.  
        /// 
        /// I need to clean that up, possibly wrap the SOAP proxies into a class and then have a 
        /// single WiEModelRemoteSSDS implementation that uses the appropriate wrapper for the SitkaService.
        /// </remarks>
        /// <param name="p_entity"></param>
        /// <returns></returns>
        private Dictionary<string, object> EntityToPropertyBag(Entity p_entity)
        {
            Dictionary<string,object> propertyBag = new Dictionary<string,object>();

            foreach(ArrayOfKeyValueOfstringanyTypeKeyValueOfstringanyType kvp in p_entity.Properties)
            {
                propertyBag.Add(kvp.Key,kvp.Value);
            }

            return (propertyBag);
        }


        /// <summary>
        /// Helper function that saves a populated entity to the authority and container within SSDS by calling the web service proxy.
        /// </summary>
        /// <remarks>
        /// 07-13-2008 O.S.M - Updated to fix bug with update.  I was not passinging in an EntityScope when doing update.
        /// </remarks>
        /// <param name="p_entity"></param>
        /// <returns></returns>
        private bool SaveEntity(Entity p_entity)
        {
            bool bReturnValue = true;

            try
            {
                Scope containerScope = new Scope();
                containerScope.AuthorityId = AUTHORITY_NAME;
                containerScope.ContainerId = CONTAINER_NAME;

                // Call the web service to create the entity
                m_sitkaSoapService.Create(containerScope, p_entity);
            }
            catch (System.Web.Services.Protocols.SoapException soapEx)
            {
                // Look at the exception to see what the fault was for, it could likely be that we have already
                // created the Entity, so it is really an update (rather than a create).
                System.Diagnostics.Trace.WriteLine("SaveEntity() Insert failed, assuming need an update. Exception:" + soapEx.Detail.ToString());
                bReturnValue = false;
            }

            if (false == bReturnValue)
            {
                // Assume the failure was that the item already exists and we need to do an update
                // rather than an insert.  We will need to revisit this and possibly allow passing in
                // a hint for those cases where we know we are doing one versus the other to save on
                // one roundtrip.
                try
                {
                    Scope entityScope = new Scope();
                    entityScope.AuthorityId = AUTHORITY_NAME;
                    entityScope.ContainerId = CONTAINER_NAME;
                    entityScope.EntityId = p_entity.Id;

                    m_sitkaSoapService.Update(entityScope, p_entity);

                    bReturnValue = true;
                }
                catch (System.Web.Services.Protocols.SoapException soapEx2)
                {
                    // Ok Update failed too...
                    System.Diagnostics.Trace.WriteLine("There was a problem updating the Entity with SSDS. Exception: " + soapEx2.Detail.ToString());

                    throw new WiEModelException("SaveEntity() - There was an issue creating or saving the entity with SSDS.", soapEx2);
                }

            }

            return bReturnValue;
        }

        /// <summary>
        /// Executes an SDS Query and returns the results as a List of WiEDataObjects (i.e. a 
        /// generic collection)
        /// </summary>
        /// <param name="p_strQuery"></param>
        /// <returns></returns>
        private List<WiEDataObject> GetQueryResults(string p_strQuery)
        {
            Entity[] entities = null;
            List<WiEDataObject> queryResults = null;

            Scope containerScope = new Scope();
            containerScope.AuthorityId = AUTHORITY_NAME;
            containerScope.ContainerId = CONTAINER_NAME;

            try
            {
                // try to query SSDS for the member
                entities = m_sitkaSoapService.Query(containerScope, p_strQuery);

                if (entities.Length > 0)
                    queryResults = new List<WiEDataObject>();

                foreach (Entity currentEntity in entities)
                {
                    switch (currentEntity.Kind)
                    {
                        case LOCATION_KIND:
                            WiELocationDataObject newLocationDataObject = new WiELocationDataObject(EntityToPropertyBag(currentEntity));
                            queryResults.Add(newLocationDataObject);
                            break;
                        case MEMBER_KIND:
                            WiEMemberDataObject newMemberDataObject = new WiEMemberDataObject(EntityToPropertyBag(currentEntity));
                            queryResults.Add(newMemberDataObject);
                            break;
                        case DEVICE_KIND:
                            WiEDeviceDataObject newDeviceDataObject = new WiEDeviceDataObject(EntityToPropertyBag(currentEntity));
                            break;
                        default:
                            System.Diagnostics.Trace.WriteLine("Unsupported or deprecated entity was returned: {0}.", currentEntity.Kind);
                            break;
                    }
                }

            }
            catch (Exception ex)
            {
                // There was a problem querying for location history.
                System.Diagnostics.Trace.WriteLine("There was a problem querying data object(s): " + ex.ToString());
            }

            return queryResults;
        }

        /// <summary>
        /// Helper function to create the Authority if it does not already exist for the WhereIsEd Service
        /// </summary>
        /// <returns>true if successful</returns>
        private bool InitializeAuthority()
        {
            bool bReturnValue = true;

            Scope serviceScope = new Scope();

            Authority authority = new Authority();

            authority.Id = AUTHORITY_NAME;

            try
            {
                // Call the web service to create the authority
                m_sitkaSoapService.Create(serviceScope, authority);
            }
            catch (System.Web.Services.Protocols.SoapException createAuthoritySoapException)
            {
                // Look at the exception to see what the fault was for, it could likely be that we have already
                // created the authority, so this is not an issue.
                System.Diagnostics.Trace.WriteLine("Problem creating the authority.  Exception: " + createAuthoritySoapException.ToString());
            }
            catch (Exception createAuthorityException)
            {
                // There was an issue trying to create the authority
                bReturnValue = false;
                System.Diagnostics.Trace.WriteLine("There was an issue creating / initializing Authority: " + createAuthorityException.ToString());
            }

            return bReturnValue;
        }

        /// <summary>
        /// Helper function to initialize the Where is Ed container within the where is ed authority.
        /// </summary>
        /// <returns></returns>
        private bool InitializeContainer()
        {
            bool bReturnValue = true;

            Scope serviceScope = new Scope();
            serviceScope.AuthorityId = AUTHORITY_NAME;

            Container container = new Container();
            container.Id = CONTAINER_NAME;

            try
            {
                // Call the web service to create the authority
                m_sitkaSoapService.Create(serviceScope, container);
            }
            catch (System.Web.Services.Protocols.SoapException)
            {
                // Look at the exception to see what the fault was for, it could likely be that we have already
                // created the authority, so this is not an issue.
                //
                // For now Assume this was a fault because entity already exists...
                //
                // For some reason the marshaller is not recognizing the error type so we have
                // to look at the Detail <Error ...> and figure it out.
                //
                //      string statusCode = soapEx.Detail["Error"]["StatusCode"].Value;
                //      if (statusCode == "EntityExists")...
            }
            catch (Exception ex)
            {
                // There was an issue trying to create the authority, it may already exist.
                bReturnValue = false;
                System.Diagnostics.Trace.WriteLine("There was an issue creating / initializing Container: " + ex.ToString());
            }

            return (bReturnValue);
        }


        /// <summary>
        /// Checks to see that container and authority exist and are initialized.
        /// </summary>
        /// <returns></returns>
        private bool IsInitialized()
        {
            bool bReturnValue = false;

            try
            {
                Scope authorityScope = new Scope();
                authorityScope.AuthorityId = AUTHORITY_NAME;

                string strQuery = @"from e in entities where e.Id==""" + CONTAINER_NAME + @""" select e";

                // try to query SSDS for the member
                Entity[] entities = m_sitkaSoapService.Query(authorityScope, strQuery);

                bReturnValue = (entities.Length > 0);
            }
            catch (Exception ex)
            {
                // There was a problem querying for location history.
                System.Diagnostics.Trace.WriteLine("There was a problem querying for the authority's list of containers: " + ex.ToString());
            }

            return bReturnValue;
        }

        #region IWiEModel Members

        /// <summary>
        /// Event raised when the location history table has been updated (i.e. new item added / removed, etc.)
        /// </summary>
        public event EventHandler<LocationHistoryChangedArgs> RaiseLocationHistoryChangedEvent;

        /// <summary>
        /// Event raised when the device table has been updated (i.e. new item added / removed, etc.)
        /// </summary>
        public event EventHandler<DeviceChangedArgs> RaiseDeviceChangedEvent;

        /// <summary>
        /// Event raised when the member table has been updated (i.e. new item added / removed, etc.)
        /// </summary>
        public event EventHandler<MemberChangedArgs> RaiseMemberChangedEvent;


        /// <summary>
        /// Saves a Location Data Object to the SSDS data store as a Location Entity (authority/container/entity)
        /// </summary>
        /// <param name="p_locationDataObject"></param>
        public void SaveLocation(WiELocationDataObject p_locationDataObject)
        {

            // Create a new entity object and initialize it
            Entity locationHistoryEntity = DataObjectToEntity(p_locationDataObject);
            locationHistoryEntity.Kind = LOCATION_KIND;
            locationHistoryEntity.Id = p_locationDataObject.LocationID.ToString();

            // Attempt to save the location history
            if (false == SaveEntity(locationHistoryEntity))
            {
                // We were unable to save the location to the remote data store...
                throw new Exception("Unable to save location information.");
            }
            else
            {
                // We saved the record, inform anyone that cares that we posted a new location
                if (RaiseLocationHistoryChangedEvent != null)
                    RaiseLocationHistoryChangedEvent(this, new LocationHistoryChangedArgs(p_locationDataObject));
            }
        }

        /// <summary>
        /// Initializes the SSDS implementation of the data model.
        /// </summary>
        public void Initialize()
        {
            // Initialize the web service proxy
            m_sitkaSoapService = new SitkaSoapService();
            m_sitkaSoapService.Credentials = new System.Net.NetworkCredential(LOGIN_NAME,LOGIN_PASSWORD);

            // Check to see if the authority and containers exist, if not create them
            if (!IsInitialized())
            {
                // Make sure we have an Authority and Container to store our information in SSDS
                InitializeAuthority();
                InitializeContainer();
            }
        }

        /// <summary>
        /// Cleans up any resources allocated for the SSDS implementation of the data model.
        /// </summary>
        public void Terminate()
        {
            if (m_sitkaSoapService != null)
                m_sitkaSoapService = null;
        }

        /// <summary>
        /// Retrieves the location specified in p_guidLocationID from the SSDS data store.
        /// </summary>
        /// <param name="p_guidLocationID"></param>
        /// <returns></returns>
        public WiELocationDataObject GetLocation(Guid p_guidLocationID)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Removes the specificied location (with ID of p_guidLocationID) from the SSDS data store.
        /// </summary>
        /// <param name="p_guidLocationID"></param>
        public void RemoveLocation(Guid p_guidLocationID)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Retrieves the location history for a given time period (from start to end time) from the SSDS data store.
        /// </summary>
        /// <param name="p_dtStartTimeUTC"></param>
        /// <param name="p_dtStopTimeUTCUTC"></param>
        /// <returns></returns>
        public List<WiELocationDataObject> GetLocationHistoryBetween(DateTime p_dtStartTimeUTC, DateTime p_dtStopTimeUTC)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Retrieves the location history for the time period "before" p_dtEndTimeUTC (GMT or UTC Time).
        /// </summary>
        /// <param name="p_dtEndTimeUTC"></param>
        /// <returns></returns>
        public List<WiELocationDataObject> GetLocationHistoryBefore(DateTime p_dtEndTimeUTC)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Saves a member to the remote data store (Insert / Add)
        /// </summary>
        /// <param name="p_memberDataObject"></param>
        public void SaveMember(WiEMemberDataObject p_memberDataObject)
        {
            // Create a new entity object and initialize it
            Entity memberEntity = DataObjectToEntity(p_memberDataObject);
            memberEntity.Kind = MEMBER_KIND;
            memberEntity.Id = p_memberDataObject.MemberID.ToString();

            // Attempt to save the location history
            if (false == SaveEntity(memberEntity))
            {
                // We were unable to save the location to the remote data store...
                throw new WiEModelException("SaveMember() - Unable to save member information for member:" + p_memberDataObject.MemberID.ToString());
            }
            else
            {
                // We saved the record, inform anyone that cares that we posted a new member
                if (RaiseMemberChangedEvent != null)
                    RaiseMemberChangedEvent(this, new MemberChangedArgs(p_memberDataObject));
            }
        }

        /// <summary>
        /// Returns the MemberDataObject for the member with unique ID p_guidMemberID
        /// </summary>
        /// <param name="p_guidMemberID"></param>
        /// <returns></returns>
        public WiEMemberDataObject GetMember(Guid p_guidMemberID)
        {
            WiEMemberDataObject matchingMember = null;

            string queryString = @"from e in entities where (e.Kind == """ + MEMBER_KIND + @""") && (e[""" +
                            WiEMemberDataObject.KEY_MEMBERID + @"""] == """ + p_guidMemberID.ToString() + @""") select e";


            List<WiEDataObject> matchingMembers = GetQueryResults(queryString);

            if ((null != matchingMembers) && (matchingMembers.Count > 0))
                matchingMember = (WiEMemberDataObject) matchingMembers[0];

            // Return the results
            return (matchingMember);
        }

        /// <summary>
        /// Retrieves the member whose user name matches p_strUserName.
        /// </summary>
        /// <param name="p_strUserName"></param>
        /// <returns></returns>
        public WiEMemberDataObject GetMemberByUserName(string p_strUserName)
        {
            WiEMemberDataObject matchingMember = null;

            string queryString = @"from e in entities where (e.Kind == """ + MEMBER_KIND + @""") && (e[""" +
                            WiEMemberDataObject.KEY_USERNAME + @"""] == """ + p_strUserName.ToString() + @""") select e";


            List<WiEDataObject> matchingMembers = GetQueryResults(queryString);

            if ((null != matchingMembers) && (matchingMembers.Count > 0))
                matchingMember = (WiEMemberDataObject) matchingMembers[0];

            // Return the results
            return (matchingMember);
        }

        /// <summary>
        /// Retrieves the member whose phone number matches p_strPhoneNumber
        /// </summary>
        /// <param name="p_strPhoneNumber"></param>
        /// <returns></returns>
        public WiEMemberDataObject GetMemberByPhoneNumber(string p_strPhoneNumber)
        {
            WiEMemberDataObject matchingMember = null;

            string queryString = @"from e in entities where (e.Kind == """ + MEMBER_KIND + @""") && (e[""" +
                            WiEMemberDataObject.KEY_PHONENUMBER + @"""] == """ + p_strPhoneNumber.ToString() + @""") select e";


            List<WiEDataObject> matchingMembers = GetQueryResults(queryString);

            if ((null != matchingMembers) && (matchingMembers.Count > 0))
                matchingMember = (WiEMemberDataObject) matchingMembers[0];

            // Return the results
            return (matchingMember);
        }

        /// <summary>
        /// Saves the DeviceDataObject to the SSDS data store (insert or update).
        /// </summary>
        /// <param name="p_deviceDataObject"></param>
        public void SaveDevice(WiEDeviceDataObject p_deviceDataObject)
        {
            // Create a new entity object and initialize it
            Entity deviceEntity = DataObjectToEntity(p_deviceDataObject);
            deviceEntity.Kind = DEVICE_KIND;
            deviceEntity.Id = p_deviceDataObject.DeviceID.ToString();

            // Attempt to save the location history
            if (false == SaveEntity(deviceEntity))
            {
                // We were unable to save the device to the remote data store...
                throw new WiEModelException("SaveDevice() - Unable to save device information to the SSDS data store.");
            }
            else
            {
                // We saved the record, inform anyone that cares that we posted a new device
                if (RaiseDeviceChangedEvent != null)
                    RaiseDeviceChangedEvent(this, new DeviceChangedArgs(p_deviceDataObject));
            }
        }

        /// <summary>
        /// Returns the DeviceDataObject associated with the device id in p_guidDeviceID
        /// </summary>
        /// <param name="p_guidDeviceID"></param>
        /// <returns></returns>
        public WiEDeviceDataObject GetDevice(Guid p_guidDeviceID)
        {
            WiEDeviceDataObject matchingDevice = null;

            string queryString = @"from e in entities where (e.Kind == """ + DEVICE_KIND + @""") && (e[""" +
                            WiEDeviceDataObject.KEY_DEVICEID + @"""] == """ + p_guidDeviceID.ToString() + @""") select e";

            List<WiEDataObject> matchingDevices = GetQueryResults(queryString);

            if ((null != matchingDevices) && (matchingDevices.Count > 0))
                matchingDevice = (WiEDeviceDataObject) matchingDevices[0];

            return (matchingDevice);
        }

        /// <summary>
        /// Returns the list of devices associated with the member specified by p_guidMemberID
        /// </summary>
        /// <param name="p_guidMemberID"></param>
        /// <returns></returns>
        public List<WiEDeviceDataObject> GetDevicesByMember(Guid p_guidMemberID)
        {
            List<WiEDeviceDataObject> matchingDevices = null;

            string queryString = @"from e in entities where (e.Kind == """ + DEVICE_KIND + @""") && (e[""" +
                            WiEDeviceDataObject.KEY_MEMBERID + @"""] == """ + p_guidMemberID.ToString() + @""") select e";

            // Attempt to query for any matching devices
            List<WiEDataObject> matchingDataObjects = GetQueryResults(queryString);
            if ((null != matchingDevices) && (matchingDevices.Count > 0))
            {
                // A bit inefficient (need to rework this) to populate a typed list...  May be ok since typically 
                // it is a list of 1 item.
                foreach(WiEDataObject dataObject in matchingDataObjects)
                    matchingDevices.Add((WiEDeviceDataObject) dataObject);
            }

            // return the results
            return (matchingDevices);
        }

        /// <summary>
        /// Returns true if the model is available.
        /// </summary>
        /// <returns></returns>
        public bool IsAvailable()
        {
            // The model is available if we can access the internet, we don't bother
            // if we can't get to the internet.
            return Utils.NetworkUtils.IsInternetAccessible();
        }

        public List<WiEMemberDataObject> GetMembers()
        {
            throw new NotImplementedException();
        }

        public List<WiEDeviceDataObject> GetDevices()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}
